-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use token list of accounts for user authorization #240
Conversation
Codecov Report
Continue to review full report at Codecov.
|
test/test_helper.rb
Outdated
@@ -74,7 +76,7 @@ def initialize(res, scopes, explicit_user_id = nil) | |||
end | |||
|
|||
def authorized?(r, s = nil) | |||
resource == r.to_s && (s.nil? || scopes.include?(s.to_s)) | |||
authorized_resources.keys.include?(r) && (s.nil? || authorized_resources.values.flatten.include?(s.to_s)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long. [109/100]
@@ -3,7 +3,7 @@ | |||
describe Api::AuthorizationsController do | |||
|
|||
let (:user) { create(:user) } | |||
let (:token) { OpenStruct.new.tap { |t| t.user_id = user.id } } | |||
let (:token) { StubToken.new(user.individual_account.id, "admin", user.id) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer single-quoted strings when you don't need string interpolation or special symbols.
member_account.id => "member", | ||
individual_account.id => "admin" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} at 17, 4 is not aligned with StubToken.new(nil, nil, user.id).tap { |t| at 12, 17 or let (:token) { StubToken.new(nil, nil, user.id).tap { |t| at 12, 2.
let (:token) { StubToken.new(nil, nil, user.id).tap { |t| | ||
t.authorized_resources = { | ||
member_account.id => "member", | ||
individual_account.id => "admin" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer single-quoted strings when you don't need string interpolation or special symbols.
|
||
let (:token) { StubToken.new(nil, nil, user.id).tap { |t| | ||
t.authorized_resources = { | ||
member_account.id => "member", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer single-quoted strings when you don't need string interpolation or special symbols.
let (:member_account) { create(:account) } | ||
let (:unapproved_account) { create(:account)} | ||
|
||
let (:token) { StubToken.new(nil, nil, user.id).tap { |t| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using {...} for multi-line blocks.
Block body expression is on the same line as the block start.
let (:random_account) { create(:account) } | ||
before { unapproved_account.memberships.first.update!(approved: false) } | ||
let (:member_account) { create(:account) } | ||
let (:unapproved_account) { create(:account)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing inside }.
app/models/user.rb
Outdated
Series. | ||
joins('LEFT OUTER JOIN `memberships` ON `memberships`.`account_id` = `series`.`account_id`'). | ||
where(['memberships.user_id = ? and memberships.approved is true', id]) | ||
!approved_accounts.nil? ? Series.where({ account_id: approved_accounts.try(:ids) }) : [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant curly braces around a hash parameter.
app/models/user.rb
Outdated
Story. | ||
joins('LEFT OUTER JOIN `memberships` ON `memberships`.`account_id` = `pieces`.`account_id`'). | ||
where(['memberships.user_id = ? and memberships.approved is true', id]) | ||
!approved_accounts.nil? ? Story.where({ account_id: approved_accounts.try(:ids) }) : [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant curly braces around a hash parameter.
end | ||
|
||
def authenticate_user! | ||
user_not_authorized unless current_user | ||
end | ||
|
||
def get_authorized_resources | ||
if prx_auth_token.authorized_resources | ||
current_user.approved_accounts ||= Account.where({ id: prx_auth_token.authorized_resources.keys }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant curly braces around a hash parameter.
Line is too long. [104/100]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I want a 2nd opinion on this one from Ryan, as he also worked on these authenticated controllers. @cavis can you take a look see at the code and my concerns?
def get_authorized_resources | ||
token_accounts = prx_auth_token.authorized_resources.try(:keys) | ||
if token_accounts | ||
current_user.approved_accounts ||= Account.where(id: token_accounts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we want the user model, which is retrieved from the DB and is in the model layer, also aware of the token and its contents which come from the controller layer. Seems like we are having controller / request logic and information leak into the model which should not be aware of or rely on that layer.
The other part of this is that it is could be misleading, because the user may have a different set of accounts they can access than is allowed by and listed in the token.
For example, you could generate a token with only a subset of the actual accounts the user has access to, or using client credentials, get a different account entirely in aur
than is reflected in the user's account membership.
So the more I think about it, it seems like with the move to get authorized accounts from the token, not the user, they become 2 different things - there is the user that logged in, and the list of accounts authorized in the token, and they are not necessarily related except for having both been identified in the token.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious what @cavis might say about this as well - with the separation of accounts and users, where the token provides the list of accounts not the user, I think it makes sense to move these methods using the list of accounts out of the user model, perhaps into methods on the ApiAuthenticated
concern, or just in the relevant controller?
app/models/user.rb
Outdated
Story. | ||
joins('LEFT OUTER JOIN `memberships` ON `memberships`.`account_id` = `pieces`.`account_id`'). | ||
where(['memberships.user_id = ? and memberships.approved is true', id]) | ||
!approved_accounts.nil? ? Story.where(account_id: approved_accounts.try(:ids)) : [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could Story.where(account_id: approved_accounts.try(:ids))
be in a controller method instead of in the user model? doesn't seem to be using anything in the user model, except the convenience that it is the resource
loaded by authorized controllers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could certainly be in there! I'm not quite sure where to place the method definitions for approved_account_series
and approved_account_stories
because they are called in the representers, which would lead me to want to put them in the model layer, but also are called in the controller...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, that is annoying - looks like they do get used for counts in the authorization_representer, but only that one representer at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can talk about it when I get in - one way to handle this may be to create an authorization
resource based on the token, which could encompass both access to the user, and the list of accounts. That's sorta what feeder does, if you have a look at that, where the auth controller has this for the resource:
def resource
Authorization.new(prx_auth_token)
end
And then the authorization model is basically a wrapper around the token, accessing the related user(well, user_id, feeder would have to make a remote call to getthe user), and could also return the approved_accounts and such:
class Authorization
include HalApi::RepresentedModel
attr_accessor :token
def initialize(token)
@token = token
end
def user_id
token.user_id
end
...
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would separate the idea of what info comes from the token and it's list of accounts vs. the user and the list of all member accounts, might be better?
…ed on token account IDs
app/models/authorization.rb
Outdated
get_authorized_resources | ||
end | ||
|
||
def get_authorized_resources |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put empty method definitions on a single line.
@@ -47,7 +47,7 @@ def create_resource | |||
story.creator_id = current_user.id | |||
story.account_id ||= story.series.try(:account_id) | |||
story.account_id ||= current_user.account_id | |||
story.account_id ||= current_user.approved_accounts.first.try(:id) | |||
story.account_id ||= current_user.approved_accounts.first.try(:id) # not sure if I should change this to look at token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long. [124/100]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in the stories controller -- not sure whether to change this to just use the first account ID listed on the auth token, or perhaps the first account ID with role "admin", or else to leave it as is -- calling the User model, which joins against memberships. (Or should this simply be the user's individual_account
? )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is the fallback for the fallback (when there is neither a series.account nor a default user account), so I think it's fine to fallback on the first account.
Creating a story doesn't require :admin permissions, so I don't think we need to check that.
There is another issue here though which is that the default account or series account may not be in the token's list of accounts, so we should check for that too - we shouldn't set the account to a value that the token does not include, does that make sense?
@kookster this more along the lines of what you're thinking? |
@@ -0,0 +1,23 @@ | |||
# encoding: utf-8 | |||
|
|||
class Authorization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this whole class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couple of things, smallish
test/models/user_test.rb
Outdated
create(:account, user: user, stories_count: 2) | ||
user.approved_account_stories.count.must_equal start_count + 2 | ||
it 'has a list of stories and series' do | ||
user.approved_account_stories.must_include story |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you leaving the User methods like approved_account_stories
, even though they aren't being used anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
approved_account_stories
and approved_account_series
are both used in the Authorization Representer. Do you think that representer should be looking at the stories + series associated with the accounts on the token instead?
@@ -12,6 +12,9 @@ def current_user | |||
def user_not_authorized | |||
raise 'user_not_authorized' | |||
end | |||
|
|||
def prx_auth_token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put empty method definitions on a single line.
@@ -4,7 +4,9 @@ | |||
|
|||
let(:user) { create(:user) } | |||
let(:account) { user.default_account } | |||
let(:representer) { Api::AuthorizationRepresenter.new(user) } | |||
let(:token) { StubToken.new(account.id, 'admin', user.id)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing inside }.
end | ||
end | ||
|
||
def create_resource | ||
super.tap do |story| | ||
story.creator_id = current_user.id | ||
story.account_id ||= story.series.try(:account_id) | ||
story.account_id ||= current_user.account_id | ||
story.account_id ||= current_user.approved_accounts.first.try(:id) | ||
story.account_id ||= current_user.account_id if authorization.authorized?(current_user.default_account) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long. [109/100]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for adding that check - clever to use the authorized?
method, I was thinking of doing a detect on the account list - this is much better
@token = token | ||
end | ||
|
||
def id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this because the AuthorizationRepresenter wants an id to build links ... misguided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does it want the id for? the self
link? I don't think you should need to provide an id for the representer, can we track this down?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if it is the self
link, which is all I can think of, we can override the self_link
method on the representer to not need this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bug in hal gem - PRX/hal_api-rails#8
default_account.id | ||
end | ||
|
||
def default_account |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly, added this because AuthorizationRepresenter wants to return a default_account
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, makes sense to me
ok, back to you @kookster ! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if you can get rid of needing the id attribute on the Authorization model and still make the representer happy.
end | ||
end | ||
|
||
def create_resource | ||
super.tap do |story| | ||
story.creator_id = current_user.id | ||
story.account_id ||= story.series.try(:account_id) | ||
story.account_id ||= current_user.account_id | ||
story.account_id ||= current_user.approved_accounts.first.try(:id) | ||
story.account_id ||= current_user.account_id if authorization.authorized?(current_user.default_account) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for adding that check - clever to use the authorized?
method, I was thinking of doing a detect on the account list - this is much better
@token = token | ||
end | ||
|
||
def id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does it want the id for? the self
link? I don't think you should need to provide an id for the representer, can we track this down?
@token = token | ||
end | ||
|
||
def id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if it is the self
link, which is all I can think of, we can override the self_link
method on the representer to not need this.
default_account.id | ||
end | ||
|
||
def default_account |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, makes sense to me
end | ||
|
||
def token_auth_stories | ||
Story.where(account_id: token_auth_accounts.try(:ids)) unless token_auth_accounts.nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need the try
when you have the unless nil?
already? Fine to leave it this way, I am a belt and suspenders kind of person myself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just doing belt + suspenders!
end | ||
|
||
it 'checks against token to see if accounts are authorized' do | ||
authorization.authorized?(account).must_equal true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you could also write this as:
authorization.must_be :authorized?, account
just a point of information - this test is great
#216
test_helper.rb
to more closely mimic auth token behavior in terms of authorized_resources list